import React, { useState, useCallback, useRef, useEffect } from "react";
import ReactFlow, { addEdge, useNodesState, useEdgesState, useReactFlow } from "reactflow";

import "./CSS/app.css";
import TheCustomNodeType from "./components/TheCustomNodeType";
import TheAddNodeSidebar from "./components/TheAddNodeSidebar";
import TheAddNodeDialog from "./components/TheAddNodeDialog";
import TheNodePanelTemplate from "./components/TheNodePanelTemplate";

import "react-flow-renderer/dist/style.css";
import "react-flow-renderer/dist/theme-default.css";
import Fab from "@mui/material/Fab";
import { Button } from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";

export default function App() {
  const [selectedNodeId, setSelectedNodeId] = useState(null);
  const [workFlowName, setWorkFlowName] = useState("select a workflow");
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
  const [nodeTypes, setNodeTypes] = useState({});
  const [showSidebar, setShowSidebar] = useState(true);
  const [showNodeDialog, setShowNodeDialog] = useState(true);
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  const [nodeSourceId, setNodeSourceId] = useState({})
  const [nodePose, setNodePose] = useState({ x: 100, y: 100 })
  const { getViewport, getNodes, setViewport } = useReactFlow();

  const { v4: uuidv4 } = require('uuid');
  const TriggerNodes = ['UserInputNode', 'GitlabNode'];
  const nodeConfigs = useRef([]);

  useEffect(() => {
    //console.log(nodes.length);
  }, [nodes])

  const findNodeConfig = (nodeTypeName) => {
    nodeTypeName = nodeTypeName.replace(/\d+$/, '')
    const templateConfig = nodeConfigs.current.find(
      (config) => config.NodeTypeName === nodeTypeName
    );

    if (!templateConfig) {
      return null;
    }

    return JSON.parse(JSON.stringify(templateConfig));
  };

  const updateNodeConfigs = async (newConfigs) => {
    nodeConfigs.current = newConfigs;
    const newTypes = nodeConfigs.current.reduce((acc, { NodeTypeName, OutputPortsCount }) => {
      if (OutputPortsCount === -1) {
        for (let i = 1; i < 6; i++) {
          acc[NodeTypeName + String(i)] = (props) => (
            <TheCustomNodeType
              otherValue={edges}
              handleShowCustomNode={handleNodeDialog}
              OutputPortsCount={i}
              {...props}
            />
          );
        }
      }
      else {
        acc[NodeTypeName] = (props) => (
          <TheCustomNodeType
            otherValue={edges}
            handleShowCustomNode={handleNodeDialog}
            OutputPortsCount={OutputPortsCount}
            {...props}
          />
        );
      }

      return acc;
    }, {});

    setNodeTypes(newTypes);
  }

  const updateFlowFromJSON = async (workflowJson) => {
    const { WorkflowName, TriggerNode, OperationNodes, Connections } = workflowJson;

    setWorkFlowName(WorkflowName);

    const processDynamicElements = (sourceData) => {
      if (sourceData.NodeTypeName === undefined) {
        return [];
      }
      const nodeConfig = findNodeConfig(sourceData.NodeTypeName, nodeConfigs);
      const elements = JSON.parse(JSON.stringify(nodeConfig?.dynamicElements || []));

      const processListDynamicElements = (sourceData, elementlist) => {
        let elementlistCopy = JSON.parse(JSON.stringify(elementlist));
        elementlistCopy.map((item) => item.value = sourceData[item.name]);
        return elementlistCopy;
      }
      return elements.map((element) => {
        if (element.type === "editableList") {
          return {
            ...element,
            value: sourceData[element.name]
              ? sourceData[element.name].map((item) =>
                processListDynamicElements(item, element.elementList)
              )
              : [],
          };
        }
        else {
          const value = sourceData[element.name];
          return {
            ...element,
            value: value !== undefined ? value : element.value,
          };
        }
      });
    };

    let nodeList = [];
    if (Object.keys(TriggerNode).length !== 0) {
      nodeList.push(createNewNode(TriggerNode.NodeTypeName, TriggerNode.Id, TriggerNode.CustomName, TriggerNode.nodePosX, TriggerNode.nodePosY, processDynamicElements(TriggerNode)))
    }

    OperationNodes.map((node) => nodeList.push(createNewNode(node.NodeTypeName, node.Id, node.CustomName, node.nodePosX, node.nodePosY, processDynamicElements(node))));

    const hasDuplicateIds = nodeList.some((item, index) => nodeList.findIndex(obj => obj.id === item.id) !== index);

    if (hasDuplicateIds) {
      nodeList = nodeList.reduce((acc, item) => {
        if (!acc.some(obj => obj.id === item.id)) {
          acc.push(item);
        }
        return acc;
      }, []);
      console.log("Duplicate ids when load.");
    }

    setNodes(nodeList);

    setEdges(
      Connections.map((edge) => ({
        id: edge.Id,
        source: edge.FromNodeId,
        target: edge.ToNodeId,
        sourceHandle: `${edge.FromNodeId}-right-${edge.FromNodeOutputPortIndex}`,
        targetHandle: `${edge.ToNodeId}-left-${edge.ToNodeInputPortIndex}`,
      }))
    );
  };

  window.updateNodeConfigs = updateNodeConfigs;
  window.updateFlowFromJSON = updateFlowFromJSON;

  const saveWorkFlow = () => {
    let result = {
      WorkflowName: workFlowName,
      TriggerNode: {},
      OperationNodes: [],
      Connections: edges.map((edge) => ({
        Id: edge.id,
        FromNodeId: edge.source,
        FromNodeOutputPortIndex: parseInt(edge.sourceHandle.split("-").pop()),
        ToNodeId: edge.target,
        ToNodeInputPortIndex: parseInt(edge.targetHandle.split("-").pop()),
      })),
    };

    const extractRawTextFromHtml = (html) => {
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');

      const spans = doc.querySelectorAll('.user-input');
      spans.forEach(span => {
        const inputElement = span.querySelector('input[type="hidden"]');
        const uuid = inputElement ? inputElement.value : null;

        const variableName = span.querySelector('.nodeVariable')?.textContent;
        if (uuid && variableName) {
          const replaceText = `{{#${uuid}.${variableName}#}}`;
          const textNode = document.createTextNode(replaceText);
          span.parentNode.replaceChild(textNode, span);
        }
      });

      return doc.body.textContent;
    };

    const processDynamicElements = (element) => {
      let newObj = {};
      if (element.hasOwnProperty("elementList")) {
        delete element.elementList;
      }

      if (element.type === 'editableList') {
        newObj[element.name] = [];
        for (let elelist of element.value) {
          let newEleObj = {};
          for (let ele of elelist) {
            Object.assign(newEleObj, processDynamicElements(ele));
          }
          newObj[element.name].push(newEleObj);
        }
      } else if (element.type === "enum") {
        newObj[element.name] = parseInt(element.value);
      } else if (element.type === "text" || element.type === "listVariable") {
        newObj[element.name] = extractRawTextFromHtml(element.value);
      } else {
        newObj[element.name] = element.value;
      }

      return newObj;
    };

    const nodesCopy = JSON.parse(JSON.stringify(nodes));

    nodesCopy.forEach((node) => {
      let nodeDetail = {
        NodeTypeName: node.type,
        Id: node.id,
        Type: node.typeDetail,
        CustomName: node.data.label,
        nodePosX: node.position.x,
        nodePosY: node.position.y,
      };

      if (node.dynamicElements) {
        node.dynamicElements.forEach((element) => {
          let processedElement = processDynamicElements(element);
          Object.assign(nodeDetail, processedElement);
        });
      }

      if (TriggerNodes.includes(node.type)) {
        result.TriggerNode = nodeDetail;
      } else {
        result.OperationNodes.push(nodeDetail);
      }
    });

    const hasDuplicateIds = result.OperationNodes.some((item, index) => result.OperationNodes.findIndex(obj => obj.Id === item.Id) !== index);

    if (hasDuplicateIds) {
      result.OperationNodes = result.OperationNodes.reduce((acc, item) => {
        if (!acc.some(obj => obj.Id === item.Id)) {
          acc.push(item);
        }
        return acc;
      }, []);
      console.log("Duplicate ids when save.");
    }

    window.chrome.webview.postMessage(JSON.stringify(result));
  };

  const handleShowSidebar = () => {
    setShowSidebar(!showSidebar);
  };

  const handleNodeDialog = (event) => {
    setNodeSourceId({
      source: event.nativeEvent.target.dataset.nodeid,
      sourceHandle: event.nativeEvent.target.dataset.handleid
    });
    const { clientX, clientY } = event;
    setMousePosition({ x: clientX, y: clientY });
    setShowNodeDialog(false);
    setSelectedNodeId(null);
  };

  const getParentNodeVariables = useCallback(() => {
    const selectedNode = nodes.find((node) => node.id === selectedNodeId);
    if (!selectedNode) return [];

    const uniqueVariables = new Map();
    const recursiveFetch = (nodeId, visited = new Set()) => {
      if (visited.has(nodeId)) return;
      visited.add(nodeId);

      const parentNodeIds = edges
        .filter((edge) => edge.target === nodeId)
        .map((edge) => edge.source);

      parentNodeIds.forEach((parentId) => {
        const parentNode = nodes.find((node) => node.id === parentId);
        if (!parentNode) return;

        parentNode.OutputVariable?.forEach((variable) => {
          const key = `${parentNode.id}_${variable}`;
          if (!uniqueVariables.has(key)) {
            uniqueVariables.set(key, {
              id: parentNode.id,
              nodeLabel: parentNode.data.label,
              variableName: variable,
            });
          }
        });

        if (parentNode.type === "ParameterExtractionNode") {
          parentNode.dynamicElements[1].value.forEach((outPutVariable) => {
            const variable = outPutVariable[1].value;
            const key = `${parentNode.id}_${variable}`;
            if (!uniqueVariables.has(key)) {
              uniqueVariables.set(key, {
                id: parentNode.id,
                nodeLabel: parentNode.data.label,
                variableName: variable,
              });
            }
          });
        }

        recursiveFetch(parentId, visited);
      });
    };

    recursiveFetch(selectedNodeId);
    return Array.from(uniqueVariables.values());
  }, [nodes, edges, selectedNodeId]);

  const onConnect = useCallback(
    (params) => {
      const sourceNode = nodes.find((node) => node.id === params.source);
      console.log(params)
      let edgeToDelete;
      if (sourceNode?.type === "AndNode" || sourceNode?.type === "OrNode") {
        edgeToDelete = edges.filter(
          (edge) => edge.targetHandle === params.targetHandle
        );
      }
      else {
        edgeToDelete = edges.filter(
          (edge) => edge.sourceHandle === params.sourceHandle || edge.targetHandle === params.targetHandle
        );
      }

      if (edgeToDelete) {
        setEdges((edges) => edges.filter((edge) => !edgeToDelete.includes(edge)));
      }

      const edgeWithSameId = edges.find((edge) => edge.id === params.id);
      if (!edgeWithSameId) {
        setEdges((edges) => addEdge(params, edges));
      }
      console.log(edges)
    },
    [nodes, edges, setEdges]
  );

  const onParamsChange = (nodeId, newParams) => {
    setNodes((nds) =>
      nds.map((node) => {
        if (node.id === nodeId) {
          const lastChar = node.type.charAt(node.type.length - 1);
          if (!isNaN(lastChar)) {
            const item = newParams.find(item => item.type === "editableList");
            if (item && item.value.length < 6) {
              node.type = node.type.replace(/\d$/, item.value.length);
            }
          }
          return {
            ...node,
            dynamicElements: newParams,
          };
        }
        return node;
      })
    );
  };

  const renderNodePanel = () => {
    const selectedNode = nodes.find((node) => node.id === selectedNodeId);
    if (!selectedNode) {
      return null;
    }

    const handleParamsChange = (newParams) => {
      onParamsChange(selectedNodeId, newParams);
    };

    const handleNodeNameChange = (nodeId, newNodeName) => {
      setNodes((prevNodes) =>
        prevNodes.map((node) =>
          node.id === nodeId ? { ...node, data: { ...node.data, label: newNodeName } } : node
        )
      );
    };

    const handleClose = () => {
      setSelectedNodeId(null);
    };

    return (
      <TheNodePanelTemplate
        NodeName={selectedNode.data.label}
        NodeType={selectedNode.type}
        onNodeNameChange={(newNodeName) => handleNodeNameChange(selectedNodeId, newNodeName)}
        dynamicElements={selectedNode.dynamicElements}
        onParamsChange={handleParamsChange}
        variables={getParentNodeVariables()}
        OutputVariable={selectedNode.OutputVariable}
        handleClose={handleClose}
      />
    );
  };

  const createEdgeFromNode = (fromNode, newNode) => {
    if (fromNode && fromNode.source && newNode.data.InputPortsCount) {
      const existingEdgeToDelete = edges.find(
        (edge) => edge.source === fromNode.source && edge.sourceHandle === fromNode.sourceHandle
      );

      if (existingEdgeToDelete) {
        setEdges((eds) => eds.filter((edge) => edge.id !== existingEdgeToDelete.id));
      }

      const newEdge = {
        id: `reactflow__edge-${fromNode.source}${fromNode.sourceHandle}-${newNode.id}${newNode.id}-left-0`,
        source: fromNode.source,
        sourceHandle: fromNode.sourceHandle,
        target: newNode.id,
        targetHandle: `${newNode.id}-left-0`,
      };

      setEdges((eds) => addEdge(newEdge, eds));
    }
  }

  const createNodeinTheCenter = () => {
    const { x, y, zoom } = getViewport();
    return {
      centerX: -x / zoom + window.innerWidth / (2 * zoom),
      centerY: -y / zoom + window.innerHeight / (2 * zoom)
    }
  }

  const createNewNode = (type, id = null, label = null, nodePosX = null, nodePosY = null, dynamicElements = null, fromNode = null) => {
    const { centerX, centerY } = createNodeinTheCenter()

    const nodeConfig = findNodeConfig(type);
    if (!nodeConfig) {
      return null;
    }

    if (nodeSourceId) {
      fromNode = JSON.parse(JSON.stringify(nodeSourceId));
    }

    const newNodeId = id || uuidv4();
    const generateNodeLabel = (type) => {
      let label = type;
      for (let suffix = 1; nodes.some(node => node.data.label === type + (suffix === 1 ? '' : suffix)); suffix++) {
        label = type + (suffix + 1);
      }
      return label;
    };
    const nodelabel = label || generateNodeLabel(type);

    const regex = /\d+$/;
    if (nodeConfig.OutputPortsCount === -1 && !regex.test(type)) {
      type += '1';
      nodeConfig.OutputPortsCount = 1;
    }


    let newNode = {
      id: newNodeId,
      type: type,
      typeDetail: nodeConfig.TypeDetail,
      OutputVariable: nodeConfig.OutputVariable,
      dynamicElements: dynamicElements || JSON.parse(JSON.stringify(nodeConfig.dynamicElements)),
      data: {
        label: nodelabel,
        InputPortsCount: nodeConfig.InputPortsCount,
        OutputPortsCount: nodeConfig.OutputPortsCount,
      },
      position: { x: nodePosX ?? centerX, y: nodePosY ?? centerY },
    };

    const newNodeTypeInTriggerNodes = TriggerNodes.includes(newNode.type);
    if (newNodeTypeInTriggerNodes) {
      setNodes(nodes.filter(node => !TriggerNodes.includes(node.type)));
    }

    setNodes((nds) => nds.concat(newNode));

    createEdgeFromNode(fromNode, newNode);

    setShowNodeDialog(true);
    setSelectedNodeId(newNodeId);
    setNodeSourceId({});
    return newNode;
  };



  return (
    <div className="containerApp">
      <div>
        <TheAddNodeSidebar
          showSidebar={showSidebar}
          materials={nodeConfigs.current.map((config) => config.NodeTypeName)}
          onMaterialSelect={createNewNode}
        />
      </div>
      <div>
        <TheAddNodeDialog
          showSidebar={showNodeDialog}
          materials={nodeConfigs.current.map((config) => config.NodeTypeName)}
          onMaterialSelect={createNewNode}
          mousePosition={mousePosition}
        />
      </div>
      <div className="subContainerApp">
        <div className="titleStyleApp">WorkFlow</div>
        <div className="saveInput">
          <input
            className="inputSaveStyle"
            type="text"
            value={workFlowName}
            onChange={(e) => setWorkFlowName(e.target.value)}
          />
        </div>
        <div
          style={{
            position: "absolute",
            top: "10px",
            right: "20px",
          }}
        >
          <Button
            variant="contained"
            startIcon={<SaveIcon />}
            onClick={() => saveWorkFlow()}
            className="saveButton"
          >
            Save
          </Button>
        </div>
        <div style={{ position: "absolute", left: 30, paddingTop: "5px" }}>
          <Fab
            color="primary"
            aria-label="add"
            sx={{ width: "35px", height: "35px", zIndex: 1 }}
            onClick={handleShowSidebar}
          >
            {!showSidebar ? (
              <RemoveIcon sx={{ width: "20px", height: "20px" }} />
            ) : (
              <AddIcon sx={{ width: "20px", height: "20px" }} />
            )}
          </Fab>
        </div>
        <ReactFlow
          className="reactflow-style"
          nodes={nodes.map((node) => ({
            ...node,
            data: {
              ...node.data,
              isSelected: node.id === selectedNodeId,
              onChange: (id) => setSelectedNodeId(id),
            },
          }))}
          onNodesChange={onNodesChange}
          edges={edges}
          onEdgesChange={onEdgesChange}
          onConnect={onConnect}
          nodeTypes={nodeTypes}
          fitView
        >
          {selectedNodeId && showNodeDialog && (
            <div className="selectedNodeStyle">
              <div>{renderNodePanel()}</div>
            </div>
          )}
        </ReactFlow>
      </div>
    </div>
  );
}
